import { Program } from './program.js';
import { RENDER_ORDER } from './material.js';
import { RenderObject, RenderAttribute, RenderUniform, RenderDrawInfo, RenderTexture, RenderSampler, RenderBuffer } from './renderObject.js';
import { vec3 } from '../gl-matrix/index.js';

const PRECISION_REGEX = new RegExp('precision (lowp|mediump|highp) float;');

const VERTEX_SHADER_SINGLE_ENTRY = `
uniform mat4 PROJECTION_MATRIX, VIEW_MATRIX, MODEL_MATRIX;

void main() {
    gl_Position = vertex_main(PROJECTION_MATRIX, VIEW_MATRIX, MODEL_MATRIX);
}
`;

const FRAGMENT_SHADER_ENTRY = `
void main() {
    gl_FragColor = fragment_main();
}
`;

export class Renderer {
    constructor(glAttribs, canvas) {
        this.domElement = canvas || document.createElement("canvas");
        this.glAttribs = glAttribs || { alpha: false };
        this.autoClear = true;

        this._gl = createWebGLContext(this.glAttribs, this.domElement);
        this._renderObjects = Array(RENDER_ORDER.DEFAULT);
        this._programCache = {};
        this._textureCache = {};
        this._program = null;
        this._timestamp = -1;
        this._frameDelta = 0;
        this._colliders = [];

        let gl = this._gl;

        let fragHighPrecision = gl.getShaderPrecisionFormat(gl.FRAGMENT_SHADER, gl.HIGH_FLOAT);
        this._defaultFragPrecision = fragHighPrecision.precision > 0 ? 'highp' : 'mediump';
    }

    clear() {
        this._renderObjects = Array(RENDER_ORDER.DEFAULT);
        this._colliders = [];
        this._timestamp = -1;
    }

    setSize(width, height) {
        this.domElement.width = width;
        this.domElement.height = height;
    }

    render(node, camera, stopUpdate) {
        let gl = this._gl;

        //设置渲染器
        if (node.renderer == null) {
            node.renderer = this;

            gl.clearColor(0.0, 0.0, 0.0, 1.0);
            if (this.glAttribs.depth) gl.enable(gl.DEPTH_TEST);
            gl.enable(gl.BLEND);
            gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
        }

        //是否清空画布
        if (this.autoClear) {
            gl.clear(gl.COLOR_BUFFER_BIT);
        }

        //绘制
        this._handleDraw(camera);

        if (stopUpdate) return;

        let prevTimestamp = this._timestamp;
        this._timestamp = performance.now();

        if (prevTimestamp >= 0) {
            this._frameDelta = this._timestamp - prevTimestamp;
        } else {
            this._frameDelta = 0;
        }

        //处理触发
        this._handleTrigger();

        //节点回调
        node._update(this._timestamp, this._frameDelta);
        camera._update(this._timestamp, this._frameDelta);

        //处理碰撞
        this._handleCollider();
    }

    createRenderObject(drawInfo, attributes, material, node) {
        let program = this._getMaterialProgram(material);
        let uniforms = [];
        let samplers = [];

        material.uniforms.forEach((uniform) => {
            new RenderUniform({
                name: uniform.name,
                value: uniform.value instanceof Array ? new Float32Array(uniform.value) : new Float32Array([uniform.value]),
                length: uniform.value instanceof Array ? uniform.value.length : 1
            });
        });

        material.samplers.forEach((sampler, index) => {
            if (sampler.texture) {
                let renderTexture = this._getRenderTexture(sampler.texture);
                samplers.push(new RenderSampler({ name: sampler.name, index, renderTexture }));
            }
        })

        let renderObject = new RenderObject({
            drawInfo,
            attributes,
            uniforms,
            samplers,
            program,
            node
        })

        if (!this._renderObjects[material.renderOrder]) {
            this._renderObjects[material.renderOrder] = [];
        }

        this._renderObjects[material.renderOrder].push(renderObject);

        return renderObject;
    }

    createRenderBuffer(target, data, usage) {
        let gl = this._gl;
        let buffer = gl.createBuffer();

        gl.bindBuffer(target, buffer);
        gl.bufferData(target, data, usage ? usage : gl.STATIC_DRAW);

        return new RenderBuffer({
            target,
            buffer,
            usage
        });
    }

    updateRenderBuffer(buffer, data, offset = 0) {
        let gl = this._gl;

        gl.bindBuffer(buffer.target, buffer.buffer);

        if (offset == 0) {
            gl.bufferData(buffer.target, data, buffer.usage);
        } else {
            gl.bufferSubData(buffer.target, offset, data);
        }
    }

    updateRenderTexture(renderTexture, texture) {
        let gl = this._gl;

        texture.waitForComplete().then(() => {
            gl.bindTexture(gl.TEXTURE_2D, renderTexture.texture);
            gl.texImage2D(gl.TEXTURE_2D, 0, texture.format, texture.format, gl.UNSIGNED_BYTE, texture.source);
            this._setSamplerParameters(texture);
            renderTexture.complete = true;
        });
    }

    createRenderAttribute(name, size, data) {
        let renderBuffer = data instanceof RenderBuffer ? data : this.createRenderBuffer(this._gl.ARRAY_BUFFER, new Float32Array(data));

        let result = new RenderAttribute({
            name,
            renderBuffer,
            size,
        });

        return result;
    }

    createRenderDrawInfo(count, indexData) {
        let indexBuffer = null;

        if (indexData) {
            indexBuffer = this.createRenderBuffer(this._gl.ELEMENT_ARRAY_BUFFER, new Uint16Array(indexData));
        }

        let result = new RenderDrawInfo({
            indexBuffer,
            count,
        });

        return result;
    }

    _getMaterialProgram(material) {
        let key = material.name;
        let program = null;

        if (key in this._programCache) {
            program = this._programCache[key];
        }
        else {
            let fullVertexSource = material.vertexSource + VERTEX_SHADER_SINGLE_ENTRY;

            let fragmentSource = material.fragmentSource;
            let precisionMatch = fragmentSource.match(PRECISION_REGEX);
            let fragPrecisionHeader = precisionMatch ? '' : `precision ${this._defaultFragPrecision} float;\n`;

            let fullFragmentSource = fragPrecisionHeader + fragmentSource + FRAGMENT_SHADER_ENTRY;

            program = new Program(this._gl, fullVertexSource, fullFragmentSource);

            this._programCache[key] = program;
        }

        return program;
    }

    _getRenderTexture(texture) {
        if (!texture) {
            return null;
        }

        let key = texture.textureKey;

        if (!key) {
            throw new Error('Texure does not have a valid key');
        }

        if (key in this._textureCache) {
            return this._textureCache[key];
        } else {
            let gl = this._gl;

            let renderTexture = new RenderTexture({
                texture: gl.createTexture()
            });

            this._textureCache[key] = renderTexture;

            texture.waitForComplete().then(() => {
                gl.bindTexture(gl.TEXTURE_2D, renderTexture.texture);
                gl.texImage2D(gl.TEXTURE_2D, 0, texture.format, texture.format, gl.UNSIGNED_BYTE, texture.source);
                this._setSamplerParameters(texture);
                renderTexture.complete = true;
            });

            return renderTexture;
        }
    }

    _setSamplerParameters(texture) {
        let gl = this._gl;

        let sampler = texture.sampler;
        let powerOfTwo = isPowerOfTwo(texture.width) && isPowerOfTwo(texture.height);
        let mipmap = powerOfTwo && texture.mipmap;
        if (mipmap) {
            gl.generateMipmap(gl.TEXTURE_2D);
        }

        let minFilter = sampler.minFilter || (mipmap ? gl.LINEAR_MIPMAP_LINEAR : gl.LINEAR);
        let wrapS = sampler.wrapS || (powerOfTwo ? gl.REPEAT : gl.CLAMP_TO_EDGE);
        let wrapT = sampler.wrapT || (powerOfTwo ? gl.REPEAT : gl.CLAMP_TO_EDGE);

        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, sampler.magFilter || gl.LINEAR);
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, minFilter);
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, wrapS);
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, wrapT);
    }

    _handleTrigger() {
        this._colliders.forEach((curNode) => {
            if (!curNode.visible || !curNode.boxCollider.isTrigger) return;

            for (let otherNode of this._colliders) {
                if (!otherNode.visible || otherNode == curNode) continue;//自身

                let curBox = curNode.boxCollider;
                let otherBox = otherNode.boxCollider;

                if (curBox.isTouching(otherBox)) {//接触了
                    if (curBox.colliders.some(item => item == otherBox)) {
                        //已记录， 不处理
                    }
                    else {
                        //第一次碰撞
                        curBox.colliders.push(otherBox);

                        curNode.onTriggerEnter && curNode.onTriggerEnter(otherNode);
                    }
                }
                else {
                    let index = curBox.colliders.indexOf(otherBox);

                    if (index >= 0) {
                        curBox.colliders.splice(index, 1);
                    }
                }
            }
        });
    }

    _handleCollider() {
        this._colliders.forEach((curNode) => {
            if (!curNode.visible || !curNode.boxCollider.isRigid) return;

            for (let otherNode of this._colliders) {
                if (!otherNode.visible || otherNode == curNode) continue;//自身

                let curBox = curNode.boxCollider;
                let otherBox = otherNode.boxCollider;

                let back = curBox.calculateBack(otherBox);
                if (back) {
                    vec3.add(otherNode.translation, otherNode.translation, back);
                }
            }
        });
    }

    _handleDraw(camera) {
        let program = this._program;
        let gl = this._gl;

        //循环绘制
        for (let item of this._renderObjects) {
            if (!item || !item.length) continue;

            item.forEach((renderObject) => {
                if (!renderObject.node.visible) return;

                //检查是否有纹理未加载完
                for (let sampler of renderObject.samplers) {
                    if (!sampler.renderTexture.complete) {
                        return;
                    }
                }

                //切换程序
                if (program != renderObject.program) {
                    this._program = program = renderObject.program;
                    program.use();
                }

                //绑定全局矩阵
                gl.uniformMatrix4fv(program.uniforms["PROJECTION_MATRIX"], false, camera.projectionMatrix);
                gl.uniformMatrix4fv(program.uniforms["VIEW_MATRIX"], false, camera.worldMatrixInverse);
                gl.uniformMatrix4fv(program.uniforms["MODEL_MATRIX"], false, renderObject.node.worldMatrix);

                //绑定属性
                for (let attribute of renderObject.attributes) {
                    if (attribute.name in program.attributes) {
                        let loaction = program.attributes[attribute.name];

                        gl.bindBuffer(attribute.renderBuffer.target, attribute.renderBuffer.buffer);
                        gl.enableVertexAttribArray(loaction);
                        gl.vertexAttribPointer(loaction, attribute.size, attribute.type, attribute.normalized, attribute.stride, attribute.offset);
                    }
                }

                //绑定全局变量
                for (let uniform of renderObject.uniforms) {
                    if (uniform.name in program.uniforms) {
                        let loaction = program.uniforms[uniform.name];

                        switch (uniform.length) {
                            case 1: gl.uniform1fv(loaction, uniform.value); break;
                            case 2: gl.uniform2fv(loaction, uniform.value); break;
                            case 3: gl.uniform3fv(loaction, uniform.value); break;
                            case 4: gl.uniform4fv(loaction, uniform.value); break;

                            default:
                                break;
                        }
                    }
                }

                //绑定纹理
                for (let sampler of renderObject.samplers) {
                    if (sampler.name in program.uniforms) {
                        let loaction = program.uniforms[sampler.name];

                        gl.uniform1i(loaction, sampler.index);
                        gl.activeTexture(gl.TEXTURE0 + sampler.index);
                        gl.bindTexture(gl.TEXTURE_2D, sampler.renderTexture.texture);
                    }
                }

                //最终绘制
                let drawInfo = renderObject.drawInfo;

                if (drawInfo.hasIndex) {
                    gl.bindBuffer(drawInfo.indexBuffer.target, drawInfo.indexBuffer.buffer);
                    gl.drawElements(drawInfo.mode, drawInfo.count, drawInfo.type, drawInfo.offset);
                } else {
                    gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, null);
                    gl.drawArrays(drawInfo.mode, drawInfo.first, drawInfo.count);
                }
            });
        }
    }
}

function createWebGLContext(glAttribs, canvas) {
    let contextTypes = glAttribs.webgl2 ? ['webgl2'] : ['webgl', 'experimental-webgl'];
    let context = null;

    for (let contextType of contextTypes) {
        context = canvas.getContext(contextType, glAttribs);
        if (context) {
            break;
        }
    }

    if (!context) {
        let webglType = (glAttribs.webgl2 ? 'WebGL 2' : 'WebGL');
        console.error('This browser does not support ' + webglType + '.');
        return null;
    }

    return context;
}

function isPowerOfTwo(n) {
    return (n & (n - 1)) === 0;
}